package virtual_machine;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import heap.*;
import stack.*;
import program_storage.*;

public class Vm {

	/** 
	 * My Stack, provides arguments for method Calls or provides results of operations
	 */
	static Stack myStack;
	
	/**
	 * My Heap, dynamic storage, provides the objects
	 */
	static Heap myHeap;
	
	/**
	 * My MainStorage, provides the code for all methods and additional informations
	 */
	static Main_Storage mainStorage;
	
	/**
	 * The instructionPointer, shows the current Code in the CodeList
	 */
	static int instructionPointer = 0;
	
	/**
	 * LocalVar-Array, provides the values of all local variables
	 */
	static double[] localVars;

	static boolean debug = false;
	
	static Scanner sc = new Scanner(System.in);
	
	/**
	 * Main-Method, executes the Program
	 * @param args
	 * @throws Exception
	 */
	public static void main(String[] args) throws Exception {
		if (args.length != 1) {
			throw new Exception("VM needs a classFile, but nothing found");
		}
		mainStorage = new Main_Storage(args[0]);
		myStack = new Stack();
		myHeap = new Heap();

		int localVarSize = 0;
		for (Code c : mainStorage.getMainCodeList()) {
			if ((c.getOpcode().equals(Opcode.STORE))
					|| (c.getOpcode().equals(Opcode.LOAD))) {
				if (c.getArgs()[0] > localVarSize) {
					localVarSize = (int) c.getArgs()[0];
				}
			}
		}
		Collection<List<Code>> codeListCollection = mainStorage
				.getMethodCodeMap().values();
		for (List<Code> cl : codeListCollection) {
			for (Code c : cl) {
				if ((c.getOpcode().equals(Opcode.STORE))
						|| (c.getOpcode().equals(Opcode.LOAD))) {
					if (c.getArgs()[0] > localVarSize) {
						localVarSize = (int) c.getArgs()[0];
					}
				}
			}
		}
		localVars = new double[localVarSize + 1];
		executeMain();
		System.out.println("Thank you for using SO-VM");
		System.exit(0);
	}

	/**
	 * Executes the Main Method and throws Exception, if there is an Error
	 * @throws VMException
	 * @throws Underflow
	 * @throws AdressException
	 * @throws OutOfMemory
	 */
	private static void executeMain() throws VMException, Underflow, AdressException, OutOfMemory {
		List<Code> mainCodeList = mainStorage.getMainCodeList();
		System.err.println("I'm going to execute the Main-Function --- Wait");
		execute(mainCodeList);
	}

	/**
	 * Executes the Main-Method
	 * @param myCodeList
	 * @throws VMException
	 * @throws Underflow
	 * @throws AdressException
	 * @throws OutOfMemory
	 */
	private static void execute(List<Code> myCodeList) throws VMException,
			Underflow, AdressException, OutOfMemory {
		int myPointer = instructionPointer;
		if (myCodeList.size() == 0) {
			throw new VMException("Given part is empty");
		}
		Code currentCode = myCodeList.get(myPointer);
		while (!currentCode.getOpcode().equals(Opcode.HALT)) {
			myPointer = execute(currentCode, myStack, myPointer);
			myPointer = myPointer + 1;
			currentCode = myCodeList.get(myPointer);
		}

		/***** DEBUG ******/
		System.err.println("Last element from stack: "+myStack.peek());
		System.err.println("StackSize: "+myStack.getSize());
		/***** DEBUG END **/
		
		while (!myStack.isEmpty()) {
			try {
				myStack.pop();
			} catch (Underflow u) {
				System.err
						.println("Stack Underflow after Main-Code Execution.");
			}
		}
		myHeap.empty();
		mainStorage = null;
		System.err
				.println("Code execution without Failures --- Going to Halt now");
	}

	/**
	 * Changes boolean value to integer value
	 * @param b
	 * @return
	 */
	private static int i (boolean b) {
		return b ? 1 : 0;
	}
	
	/**
	 * Executes the method
	 * @param methodCode
	 * @param methodCodeMap
	 * @return The result of the executed Method
	 * @throws VMException
	 * @throws Underflow
	 * @throws AdressException
	 * @throws OutOfMemory
	 */
	private static double[] executeMethod(List<Code> methodCode, Map<Integer, List<Code>> methodCodeMap)
			throws VMException, Underflow, AdressException, OutOfMemory {
		int methodPointer = 0;
		Stack methodStack = new Stack(myStack);
		double[] methodLocalVar = new double[localVars.length];
		for (int i = 0; i < localVars.length; i++) {
			methodLocalVar[i] = localVars[i];
		}
		if (methodCode.size() == 0) {
			throw new VMException("Method body is empty");
		}
		Code currentCode = methodCode.get(methodPointer);
		while ((!currentCode.getOpcode().equals(Opcode.RETURN))
				&& (!currentCode.getOpcode().equals(Opcode.RETURNVALUE))) {
			methodPointer = execute(currentCode, methodStack, methodPointer);
			methodPointer = methodPointer + 1;
			currentCode = methodCode.get(methodPointer);
		}
		if (currentCode.getOpcode().equals(Opcode.RETURNVALUE)) {
			try {
				return new double[] { methodStack.peek() };
			} catch (Underflow u) {
				System.err
						.println("Cannot get result from Stack because -> Underflow");
				u.printStackTrace();
				return new double[0];
			}
		}
		return new double[0];
	}
	
	private static int execute(Code co, Stack myStack, int myPointer) throws Underflow, 
			AdressException, OutOfMemory, VMException {
		double a, b, c;
		if (debug) {
			System.err.println(myStack.toString());
		}
		switch (co.getOpcode()) {
		case LDIC:
			myStack.push(co.getArgs()[0]);
			break;
		case LDDC:
			myStack.push(co.getArgs()[0]);
			break;
		case ADD:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push(a + b);
			break;
		case SUB:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push(a - b);
			break;
		case MUL:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push(a * b);
			break;
		case DIV:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push(a/b);
			break;
		case GDIV:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push(((int) a)/ ((int) b));
			break;
		case MOD:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push((int) a % (int) b);
			break;
		case AND:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push((int)a & (int)b);
			break;
		case OR:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push((int)a | (int)b);
			break;
		case XOR:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push((int)a ^ (int)b);
			break;
		case SHIFTL:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push((int)a << (int)b);
			break;
		case SHIFTR:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push((int)a >> (int)b);
			break;
		case ADDC:
			a = co.getArgs()[0];
			b = myStack.pop();
			myStack.push(a+b);
			break;
		case SUBC:
			a = co.getArgs()[0];
			b = myStack.pop();
			myStack.push(a-b);
			break;
		case SEL:
			a = myStack.pop();
			b = myStack.pop();
			c = myStack.pop();
			myStack.push((a == 1) ? b : c);
			break;
		case EQUAL:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push(i(a == b));
			break;
		case NOTEQUAL:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push(i(a != b));
			break;
		case LESS:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push(i(a < b));
			break;
		case LESSEQUAL:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push(i(a <= b));
			break;
		case GREATER:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push(i(a > b));
			break;
		case GREATEREQUAL:
			a = myStack.pop();
			b = myStack.pop();
			myStack.push(i(a >= b));
			break;
		case LOAD:
			myStack.push(localVars[(int) co.getArgs()[0]]);
			break;
		case STORE:
			a = myStack.pop();
			localVars[(int) co.getArgs()[0]] = a;
			break;
		case LENGTH:
			a = myStack.pop();
			myStack.push(myHeap.getLengthOfAnObject((int) a));
			break;
		case JUMP:
			myPointer = myPointer + (((int) co.getArgs()[0])-1);
			break;
		case JZ:
			a = myStack.pop();
			myPointer = myPointer + ((a==0) ?((int) co.getArgs()[0])-1 : 0 );
			break;
		case WRITEINT:
			a = myStack.peek();
			System.out.println((int) a);
			break;
		case WRITECHAR:
			a = myStack.peek();
			System.out.println((char) a);
			break;
		case WRITEDOUBLE:
			a = myStack.peek();
			System.out.println(a);
			break;
		case WRITEBOOL:
			a = myStack.peek();
			if (((int) a) == 1) { System.out.println("true"); } else { System.out.println("false");}
			break;
		case INTTOBOOL:
			a = myStack.pop();
			myStack.push((((int) a)==1) ? 1 : 0);
			break;
		case READINT:
			try {
				a = sc.nextInt();
			}
			catch (Exception e) {
				System.err.println("Cannot read input as an Integer, using default 0");
				a = 0;
			}
			myStack.push(a);
			break;
		case READCHAR:
			try {
				a = sc.next().charAt(0);
			}
			catch (Exception e) {
				System.err.println("Cannot read input as an Char, using default a");
				a = (int) 'a';
			}
			myStack.push(a);
			break;
		case READDOUBLE:
			try {
				a = sc.nextDouble();
			}
			catch (Exception e) {
				System.err.println("Cannot read input as an Double, using default 0.0");
				a = 0;
			}
			myStack.push(a);
			break;	
		case NEW:
			int adress = myHeap.newObject(myStack, (int) co.getArgs()[0]);
			myStack.push(adress);
			break;
		case DUP:
			a = myStack.peek();
			myStack.push(a);
			break;
		case GETVALUE:
			b = myStack.pop();
			a = myStack.pop();
			double value = myHeap.getValue((int) a, (int) b);
			myStack.push(value);
			break;
		case SETVALUE:
			a = myStack.pop();
			b = myStack.pop();
			c = myStack.pop();
			myHeap.update((int) c, (int) a, b); 
			break;
		case CALLMETH:
			a = myStack.pop();
			List<Code> mcl = mainStorage.getMethodCodeMap().get((int) a);
			if (mcl == null) {
				throw new VMException("There is no such method with this index: " + a);
			}
			double[] returnValues = executeMethod(mcl, mainStorage.getMethodCodeMap());
			int x = mainStorage.getMethodArgNumber().get((int) a);
			while (x > 0) {
				myStack.pop();
				x -= 1;
			}
			if (returnValues.length == 0) {
			}
			else if (returnValues.length == 1) {
				myStack.push(returnValues[0]);
			}
			else {
				throw new VMException("Method tried to return more than one value");
			}
			break;
		default :
			System.err.println("Unknown Instruction while executing Code");
		}
		return myPointer;
	}
	
}
